trait Material:
  def scatter(r_in: Ray, rec: HitRecord): Option[Tuple2[Ray, Vec3]]

case class Lambertian(albedo: Vec3) extends Material:
  override def scatter(r_in: Ray, rec: HitRecord): Option[Tuple2[Ray, Vec3]] =
    var scatter_direction = rec.normal + Vec3.random_in_unit_sphere
    if scatter_direction.near_zero then
      scatter_direction = rec.normal

    val scattered = Ray(rec.p, scatter_direction)
    val attenuation = albedo

    Some((scattered, attenuation))
    

case class Metal(albedo: Vec3, f: Double) extends Material:
  def fuzz: Double = if f < 1 then f else 1.0

  override def scatter(r_in: Ray, rec: HitRecord): Option[Tuple2[Ray, Vec3]] =
    val reflected = reflect(r_in.direction.unit_vector, rec.normal)
    val scattered = Ray(rec.p, reflected + Vec3.random_in_unit_sphere * fuzz)
    val attenuation = albedo

    Some((scattered, attenuation))

case class Dielectric(ir: Double) extends Material:
  val rng = scala.util.Random
  override def scatter(r_in: Ray, rec: HitRecord): Option[Tuple2[Ray, Vec3]] =
    val attenuation = Vec3(1, 1, 1)
    val refraction_ratio = if rec.front_face then 1.0 / ir else ir
    val unit_direction = r_in.direction.unit_vector
    val cos_theta = math.min(dot(unit_direction * -1.0, rec.normal), 1.0)
    val sin_theta = math.sqrt(1.0 - cos_theta * cos_theta)
    val cannot_refract = refraction_ratio * sin_theta > 1.0

    val direction =
      if cannot_refract || reflectance(cos_theta, refraction_ratio) > rng.nextDouble then
        reflect(unit_direction, rec.normal)
      else
        refract(unit_direction, rec.normal, refraction_ratio)
    val scattered = Ray(rec.p, direction)

    Some((scattered, attenuation))
